Suomi

Kattava opas TypeScript-generiikoihin, joka käsittelee niiden syntaksia, etuja, edistynyttä käyttöä ja parhaita käytäntöjä monimutkaisten datatyyppien käsittelyyn globaalissa ohjelmistokehityksessä.

TypeScript-generiikat: Monimutkaisten datatyyppien hallinta vankkoja sovelluksia varten

TypeScript, JavaScriptin supersetti, antaa kehittäjille mahdollisuuden kirjoittaa vankempaa ja ylläpidettävämpää koodia staattisen tyypityksen avulla. Yksi sen tehokkaimmista ominaisuuksista ovat generiikat, jotka mahdollistavat koodin kirjoittamisen, joka toimii erilaisten datatyyppien kanssa säilyttäen samalla tyyppiturvallisuuden. Tämä opas tarjoaa kattavan selvityksen TypeScript-generiikoista, keskittyen niiden soveltamiseen monimutkaisiin datatyyppeihin globaalin ohjelmistokehityksen kontekstissa.

Mitä generiikat ovat?

Generiikat tarjoavat tavan kirjoittaa uudelleenkäytettävää koodia, joka voi toimia eri tyyppien kanssa. Sen sijaan, että kirjoittaisit erillisiä funktioita tai luokkia jokaiselle tuettavalle tyypille, voit kirjoittaa yhden funktion tai luokan, joka käyttää tyyppiparametreja. Nämä tyyppiparametrit ovat paikkamerkkejä todellisille tyypeille, joita käytetään, kun funktiota tai luokkaa kutsutaan tai se instansioidaan. Tämä on erityisen hyödyllistä käsiteltäessä monimutkaisia tietorakenteita, joissa datan tyyppi voi vaihdella.

Generiikkojen käytön edut

Generiikkojen perussyntaksi

Generiikkojen perussyntaksiin kuuluu kulmasulkeiden (< >) käyttäminen tyyppiparametrien määrittelyyn. Nämä tyyppiparametrit nimetään tyypillisesti T, K, V jne., mutta voit käyttää mitä tahansa kelvollista tunnusta. Tässä on yksinkertainen esimerkki geneerisestä funktiosta:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Tuloste: hello
console.log(myNumber); // Tuloste: 123
console.log(myBoolean); // Tuloste: true

Tässä esimerkissä <T> määrittelee tyyppiparametrin nimeltä T. Funktio identity ottaa argumentin tyyppiä T ja palauttaa arvon tyyppiä T. Funktiota kutsuttaessa voit joko määrittää tyyppiparametrin eksplisiittisesti (esim. identity<string>) tai antaa TypeScriptin päätellä sen argumentin tyypin perusteella.

Työskentely monimutkaisten datatyyppien kanssa

Generiikoista tulee erityisen arvokkaita, kun käsitellään monimutkaisia datatyyppejä, kuten taulukoita, objekteja ja rajapintoja. Tarkastellaan joitakin yleisiä skenaarioita:

Geneeriset taulukot

Voit käyttää generiikkoja luodaksesi funktioita tai luokkia, jotka toimivat erityyppisten taulukoiden kanssa:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Tuloste: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Tuloste: apple, banana, cherry

Tässä arrayToString-funktio ottaa vastaan taulukon tyyppiä T[] ja palauttaa taulukon merkkijonoesityksen. Tämä funktio toimii minkä tahansa tyyppisten taulukoiden kanssa, mikä tekee siitä erittäin uudelleenkäytettävän.

Geneeriset objektit

Generiikkoja voidaan myös käyttää määrittelemään funktioita tai luokkia, jotka toimivat erimuotoisten objektien kanssa:


interface Person {
  name: string;
  age: number;
  country: string; // Lisätty maa globaalia kontekstia varten
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Lisätty valuutta globaalia kontekstia varten
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Tuloste: Name: Alice
displayInfo(product); // Tuloste: Name: Laptop

Tässä esimerkissä displayInfo-funktio ottaa vastaan objektin tyyppiä T, jolla on oltava name-ominaisuus tyyppiä string. Lauseke extends { name: string } on rajoite, joka määrittää vähimmäisvaatimukset tyyppiparametrille T. Tämä varmistaa, että funktio voi turvallisesti käyttää name-ominaisuutta.

Generiikkojen edistynyt käyttö

TypeScript-generiikat tarjoavat edistyneempiä ominaisuuksia, joiden avulla voit luoda entistä joustavampaa ja tehokkaampaa koodia. Tutustutaan joihinkin näistä ominaisuuksista:

Useat tyyppiparametrit

Voit määritellä funktioita tai luokkia useilla tyyppiparametreilla:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Tuloste: Bob
console.log(merged.age); // Tuloste: 42

merge-funktio ottaa kaksi objektia tyyppejä T ja U ja palauttaa uuden objektin, joka sisältää molempien objektien ominaisuudet. Tämä on tehokas tapa yhdistää dataa eri lähteistä.

Geneeriset rajoitteet

Kuten aiemmin näytettiin, rajoitteiden avulla voit rajoittaa tyyppejä, joita voidaan käyttää geneerisen tyyppiparametrin kanssa. Tämä varmistaa, että geneerinen koodi voi turvallisesti operoida määritetyillä tyypeillä.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Tuloste: 3
loggingIdentity("hello"); // Tuloste: 5
// loggingIdentity(123); // Virhe: Argumentti tyyppiä 'number' ei ole asetettavissa parametriin tyyppiä 'Lengthwise'.

loggingIdentity-funktio ottaa argumentin tyyppiä T, jolla on oltava length-ominaisuus tyyppiä number. Tämä varmistaa, että funktio voi turvallisesti käyttää length-ominaisuutta.

Geneeriset luokat

Generiikkoja voidaan käyttää myös luokkien kanssa:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Tuloste: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Tuloste: [ 2 ]

DataStorage-luokka voi tallentaa dataa mistä tahansa tyypistä T. Tämä mahdollistaa uudelleenkäytettävien ja tyyppiturvallisten tietorakenteiden luomisen.

Geneeriset rajapinnat

Geneeriset rajapinnat ovat hyödyllisiä määriteltäessä sopimuksia, jotka voivat toimia eri tyyppien kanssa. Esimerkiksi:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Result-rajapinta määrittelee geneerisen rakenteen operaation tuloksen esittämiseksi. Se voi sisältää joko dataa tyyppiä T tai virheen tyyppiä E. Tämä on yleinen malli asynkronisten tai mahdollisesti epäonnistuvien operaatioiden käsittelyyn.

Aputyypit ja generiikat

TypeScript tarjoaa useita sisäänrakennettuja aputyyppejä, jotka toimivat hyvin generiikkojen kanssa. Nämä aputyypit auttavat sinua muuntamaan ja manipuloimaan tyyppejä tehokkailla tavoilla.

Partial<T>

Partial<T> tekee kaikista tyypin T ominaisuuksista valinnaisia:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Kelvollinen

Readonly<T>

Readonly<T> tekee kaikista tyypin T ominaisuuksista vain luku -muotoisia:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Virhe: Ei voi asettaa arvoa 'age', koska se on vain luku -ominaisuus.

Pick<T, K>

Pick<T, K> valitsee joukon ominaisuuksia K tyypistä T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> poistaa joukon ominaisuuksia K tyypistä T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> luo tyypin, jossa on avaimet K ja arvot tyyppiä T:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Laajennettu lista globaalia kontekstia varten
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Laajennettu lista globaalia kontekstia varten

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Mapatut tyypit

Mapattujen tyyppien avulla voit muuntaa olemassa olevia tyyppejä iteroimalla niiden ominaisuuksien yli. Tämä on tehokas tapa luoda uusia tyyppejä olemassa olevien pohjalta. Voit esimerkiksi luoda tyypin, joka tekee toisen tyypin kaikista ominaisuuksista vain luku -muotoisia:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Virhe: Ei voi asettaa arvoa 'age', koska se on vain luku -ominaisuus.

Tässä esimerkissä [K in keyof Person] iteroi kaikkien Person-rajapinnan avainten yli, ja Person[K] hakee kunkin ominaisuuden tyypin. readonly-avainsana tekee jokaisesta ominaisuudesta vain luku -muotoisen.

Ehdolliset tyypit

Ehdollisten tyyppien avulla voit määritellä tyyppejä ehtojen perusteella. Tämä on tehokas tapa luoda tyyppejä, jotka mukautuvat erilaisiin skenaarioihin.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Käsittelee sekä nullin että undefinedin
    throw new Error("Value cannot be null or undefined");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Tuloste: HELLO

  const invalidValue = getValue(null); // Tämä aiheuttaa virheen
  console.log(invalidValue); // Tätä riviä ei saavuteta
} catch (error: any) {
  console.error(error.message); // Tuloste: Value cannot be null or undefined
}

Tässä esimerkissä NonNullable<T>-tyyppi tarkistaa, onko T null vai undefined. Jos on, se palauttaa never, mikä tarkoittaa, että tyyppi ei ole sallittu. Muuten se palauttaa T. Tämä mahdollistaa sellaisten tyyppien luomisen, jotka ovat taatusti ei-null-arvoisia.

Generiikkojen käytön parhaat käytännöt

Tässä on joitakin parhaita käytäntöjä, jotka kannattaa pitää mielessä generiikkoja käytettäessä:

Esimerkkejä globaalissa kontekstissa

Tarkastellaan joitakin esimerkkejä siitä, miten generiikkoja voidaan käyttää globaalissa kontekstissa:

Valuuttamuunnos


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Tuloste: 100 USD is equal to 85 EUR

Päivämäärän muotoilu


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));

Käännöspalvelu


interface Translation {
  [key: string]: string; // Mahdollistaa dynaamiset kieli-avaimet
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `Translation for ${key} in ${languageCode} not found.`;
  }
  return lang.translations[key] || `Translation for ${key} not found.`;
}

console.log(translate("hello", "en", languageData)); // Tuloste: Hello
console.log(translate("hello", "es", languageData)); // Tuloste: Hola
console.log(translate("welcome", "fr", languageData)); // Tuloste: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Tuloste: Translation for missingKey in de not found.

Yhteenveto

TypeScript-generiikat ovat tehokas työkalu uudelleenkäytettävän, tyyppiturvallisen koodin kirjoittamiseen, joka toimii monimutkaisten datatyyppien kanssa. Ymmärtämällä generiikkojen perussyntaksin, edistyneet ominaisuudet ja parhaat käytännöt voit parantaa merkittävästi TypeScript-sovellustesi laatua ja ylläpidettävyyttä. Kehitettäessä sovelluksia globaalille yleisölle, generiikat voivat auttaa käsittelemään erilaisia datamuotoja ja kulttuurisia käytäntöjä, varmistaen saumattoman käyttökokemuksen kaikille.